ROS2 Action 通信机制:分层架构、五路通道与状态机
ROS2 的 Action 提供「提交目标、过程反馈、最终结果」的长任务交互模式。在接口层面它只是一组客户端/服务端 API,但底层并非一条专用通道,而是由多个 Service 和 Topic 组合而成。本文基于 rclcpp_action 与 rcl_action 源码(rolling 分支)分析 Action 的完整实现:分层架构、五路通信通道的 QoS 设计、Goal 状态机、线程安全与 Executor 集成。适合已经在使用 Action 接口、想理解底层机制或排查 Action 通信问题的读者。
分层架构
Action 的实现自上而下分为四层,每层职责严格分离:
| 层级 | 职责 | 关键类型 |
|---|---|---|
| L4 用户层 | 类型安全、异步 API、用户回调 | Server<ActionT>, Client<ActionT> |
| L3 基类层 | 类型擦除、Executor 集成、事件分发 | ServerBase, ClientBase |
| L2 C 层 | 协议逻辑、状态机、生命周期管理 | rcl_action_server_t, rcl_action_client_t |
| L1 传输层 | 实际网络通信、序列化 | RMW 实现(CycloneDDS 等) |
五路通信通道
Action 底层由 3 个 Service + 2 个 Topic 共 5 路通道组成:
| 通道 | 类型 | Topic 名称 | 可靠性 | 持久性 | 历史深度 | 设计意图 |
|---|---|---|---|---|---|---|
| Goal Service | Service | /{name}/_action/send_goal | RELIABLE | VOLATILE | KEEP_LAST 10 | 目标提交不能丢失 |
| Cancel Service | Service | /{name}/_action/cancel_goal | RELIABLE | VOLATILE | KEEP_LAST 10 | 取消请求必须送达 |
| Result Service | Service | /{name}/_action/get_result | RELIABLE | VOLATILE | KEEP_LAST 10 | 结果查询必须可靠 |
| Feedback Topic | Topic | /{name}/_action/feedback | RELIABLE(默认) | VOLATILE | KEEP_LAST 10 | 过程数据可容忍丢失,可改 BEST_EFFORT 换低延迟 |
| Status Topic | Topic | /{name}/_action/status | RELIABLE | TRANSIENT_LOCAL | KEEP_LAST 1 | 新订阅者能立即获取当前状态快照 |
表中 QoS 是 rcl_action_server_get_default_options() 的默认值:三个 Service 用 rmw_qos_profile_services_default,Feedback Topic 用 rmw_qos_profile_default(RELIABLE / VOLATILE / KEEP_LAST 10),Status Topic 用 rcl_action_qos_profile_status_default(RELIABLE / TRANSIENT_LOCAL / KEEP_LAST 1)。
需要注意 Feedback Topic 的默认可靠性是 RELIABLE 而不是 BEST_EFFORT——反馈是持续的过程数据,语义上允许丢帧,用户可以自行改成 BEST_EFFORT 换取低延迟,但这需要显式配置。Status Topic 的 TRANSIENT_LOCAL + 深度 1 是「锁存」语义:任意时刻新订阅的客户端都能立刻收到最近一次状态快照,不需要等待下一次状态变化。
Service 底层同样由 DDS 的两个 Topic(Request + Reply)实现,请求与响应的关联由 RMW 实现负责。
Feedback Topic 与 Status Topic 的区别
这是最容易混淆的两个通道:
| 维度 | Feedback Topic | Status Topic |
|---|---|---|
| 消息类型 | ActionT::Impl::FeedbackMessage(含用户自定义字段) | action_msgs::msg::GoalStatusArray(纯枚举) |
| 覆盖范围 | 单个目标,消息中携带 goal_id 用于匹配 | 所有目标的状态快照列表 |
| 客户端处理 | 按 goal_id 找到对应 GoalHandle,触发用户的 feedback_callback | 遍历列表,仅调用 goal_handle->set_status(),不触发任何用户回调 |
| 触发方 | 用户代码显式调用 publish_feedback() | 框架在任意目标状态变化时自动调用 publish_status() |
| QoS | RELIABLE(默认),可改 BEST_EFFORT 换低延迟 | RELIABLE + TRANSIENT_LOCAL,可靠且可重放 |
| 语义 | 这个任务执行到了哪一步(业务进度) | 系统中哪些目标处于什么状态(生命周期) |
服务端触发 publish_status() 的时机(框架自动):
- Goal 被 ACCEPT 后(execute_goal_request_received)
- ACCEPT_AND_EXECUTE 时状态变为 EXECUTING 后
- Cancel 请求被处理,至少一个目标状态变化后(execute_cancel_request_received)
服务端触发 publish_feedback() 的时机(用户手动):
- 用户在 AcceptedCallback 的执行线程中调用 goal_handle->publish_feedback(fb) 通信时序
正常执行流程
取消目标流程
延迟结果:Result 的 Push 机制
客户端可以先请求结果,等结果就绪时由服务端主动推送——这是 Result Service 区别于普通请求-响应模式的关键设计:
多个客户端可以同时等待同一个目标的结果,服务端会向所有等待者统一推送。async_get_result() 在任务完成前调用也不会丢失:请求被暂存起来,结果就绪后逐个响应。
Goal 状态机
状态转换图
状态转换表
| 当前状态 | 触发事件 | 目标状态 | 触发方 |
|---|---|---|---|
| ACCEPTED | GOAL_EVENT_EXECUTE | EXECUTING | ACCEPT_AND_EXECUTE 响应时框架自动触发 |
| EXECUTING | GOAL_EVENT_SUCCEED | SUCCEEDED | 用户调用 goal_handle->succeed() |
| EXECUTING | GOAL_EVENT_ABORT | ABORTED | 用户调用 goal_handle->abort() |
| EXECUTING | GOAL_EVENT_CANCEL_GOAL | CANCELING | rcl_action_process_cancel_request() 内部触发 |
| CANCELING | GOAL_EVENT_CANCELED | CANCELED | 用户调用 goal_handle->canceled() |
| CANCELING | GOAL_EVENT_SUCCEED | SUCCEEDED | 任务已完成时忽略取消请求 |
| CANCELING | GOAL_EVENT_ABORT | ABORTED | 取消过程中发生错误 |
注意 CANCELING 是一个可以流向三种终态的中间状态:任务可能在取消信号到来前已经完成(SUCCEEDED),也可能在取消过程中出错(ABORTED)。
终端状态后的清理
进入 SUCCEEDED / ABORTED / CANCELED 后:
- 结果被缓存到
goal_results_[uuid],供尚未查询的客户端读取 - 通过
rcl_action_notify_goal_done()通知 rcl 层 - 超过
result_timeout后,由定时器触发execute_check_expired_goals()从所有 map 中清除。默认值存在版本差异:Iron 及之后为 10 秒(RCUTILS_S_TO_NS(10)),Humble 及之前为 15 分钟;还可配置为 -1 永久保留、0 立即丢弃。跨版本部署或依赖「事后很久还能查结果」的场景要注意这个默认值
线程安全设计
Server 侧
class ServerBaseImpl {
// 两把锁,固定加锁顺序防止死锁
// 顺序:unordered_map_mutex_ → action_server_reentrant_mutex_
std::recursive_mutex action_server_reentrant_mutex_; // 保护所有 rcl_action API 调用
std::recursive_mutex unordered_map_mutex_; // 保护三张 map
// 结果缓存:目标完成后存储,等待客户端拉取
std::unordered_map<GoalUUID, std::shared_ptr<void>> goal_results_;
// 延迟结果等待队列:客户端先请求结果但结果未就绪时暂存请求头
std::unordered_map<GoalUUID, std::vector<rmw_request_id_t>> result_requests_;
// rcl 句柄缓存:防止 rcl 内部存储释放后上层访问野指针
std::unordered_map<GoalUUID, std::shared_ptr<rcl_action_goal_handle_t>> goal_handles_;
// 原子标志:防止 execute() 对同一事件重入处理
std::atomic<bool> goal_request_ready_;
std::atomic<bool> cancel_request_ready_;
std::atomic<bool> result_request_ready_;
std::atomic<bool> goal_expired_;
}; 关键设计是 compare_exchange_strong:即使 Executor 在多线程模式下,每个事件也只被处理一次:
bool expected = true;
if (!pimpl_->goal_request_ready_.compare_exchange_strong(expected, false)) {
return; // 已被其他线程处理,直接返回
} Client 侧
std::mutex goal_handles_mutex_; // 保护 goal_handles_ map(弱引用 map)
// 结果通过 std::promise / std::shared_future 跨线程传递(天然线程安全)
// handle_feedback_message 检测 weak_ptr 悬空,自动清理过期句柄:
if (!goal_handle) {
goal_handles_.erase(goal_id); // 用户不再持有引用,自动清理
return;
} Executor 集成机制
ServerBase 和 ClientBase 均继承自 rclcpp::Waitable,通过统一接口接入 Executor 的事件循环:
Action 对 Executor 完全透明:在 Executor 看来,它只是一个普通的 Waitable,无需任何特殊调度逻辑。add_to_wait_set() 负责将 Action 的所有子实体批量注册到 wait_set 中。
关键源码索引
| 文件 | 内容 |
|---|---|
rclcpp_action/include/rclcpp_action/server.hpp | ServerBase、Server<ActionT> 定义,三个用户回调类型声明 |
rclcpp_action/include/rclcpp_action/client.hpp | ClientBase、Client<ActionT> 定义,SendGoalOptions 结构体 |
rclcpp_action/src/server.cpp | ServerBaseImpl、五路事件处理、publish_status/feedback/result 实现 |
rclcpp_action/src/client.cpp | ClientBaseImpl、handle_feedback_message/handle_status_message 实现 |
rclcpp_action/src/server_goal_handle.cpp | ServerGoalHandle 生命周期管理,succeed/abort/canceled 实现 |
rcl/rcl_action/include/rcl_action/action_server.h | rcl_action_server_options_t 默认配置,QoS 声明 |
整体类图
要点
- Action = 3 个 Service(goal / cancel / result)+ 2 个 Topic(feedback / status),QoS 默认值出自
rcl_action_server_get_default_options() - Feedback 默认 RELIABLE;想允许丢帧换低延迟需要显式改成 BEST_EFFORT。Status 是 TRANSIENT_LOCAL + 深度 1 的锁存通道,新订阅者立即拿到最新快照,且不触发用户回调
- Result Service 支持「先请求、后推送」:结果未就绪时暂存请求头,就绪后向所有等待者统一响应
- Goal 状态机里 CANCELING 可流向 SUCCEEDED / CANCELED / ABORTED 三种终态;终端结果缓存时长默认 Iron 起 10 秒、Humble 及之前 15 分钟,跨版本部署时注意
- Server / Client 对 Executor 只是普通
Waitable,事件去重靠原子标志的compare_exchange_strong